package br.mikhas.container.factory;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import br.mikhas.container.config.BeanDefinition;
import br.mikhas.container.config.Dependency;
import br.mikhas.container.config.ResourceReferenceDependency;
import br.mikhas.container.config.ValueDependecy;
import br.mikhas.container.config.Dependency.DependencyTarget;
import br.mikhas.container.config.convertion.Converter;
import br.mikhas.container.config.convertion.JavaPrimitivesConverter;
import br.mikhas.container.config.convertion.NumberConverter;
import br.mikhas.container.factory.processor.BeanFactoryProviderPostProcessor;
import br.mikhas.container.factory.processor.BeanInitializingPostProcessor;
import br.mikhas.container.factory.processor.ConverterRegisterPostProcessor;
import br.mikhas.container.factory.processor.PostProcessorRegisterPostProcessor;
import br.mikhas.container.scope.ScopedBeanPostProcessor;
import br.mikhas.reflector.ClassProxy;
import br.mikhas.reflector.ConstructorProxy;
import br.mikhas.reflector.ObjectProxy;
import br.mikhas.reflector.Reflect;
import br.mikhas.util.Assert;
import br.mikhas.util.StringUtils;

/**
 * Bean factory implementation which is able to initialize and wire a bean to
 * it's dependecies.
 * <p>
 * This bean factory is also able to add post processors on it to extend the
 * factory capabilities.
 * 
 * @author Mikhail Domanoski
 * 
 */
public class InitializingBeanFactory extends AbstractBeanFactory {

	private static final Logger log = LoggerFactory
			.getLogger(InitializingBeanFactory.class);

	private static final boolean debug = log.isDebugEnabled();

	/**
	 * List of registered converters
	 */
	private List<Converter> converters = new ArrayList<Converter>();

	/**
	 * Caches the acceptance of a converter to a type
	 */
	private Map<Class<?>, Converter> convertersCache = new HashMap<Class<?>, Converter>();

	/**
	 * List of registered bean post processors
	 */
	private List<BeanPostProcessor> beanPostProcessors = new ArrayList<BeanPostProcessor>();

	/**
	 * The {@link BeanInitializingPostProcessor}
	 */
	private BeanPostProcessor initializer;

	/*
	 * Register default converters and post processors
	 */

	public InitializingBeanFactory() {
		// Adds converters
		this.addConverter(new NumberConverter());
		this.addConverter(new JavaPrimitivesConverter());

		// Register post processors
		this
				.registerResourcePostProcessor(new BeanFactoryProviderPostProcessor());
		this
				.registerResourcePostProcessor(new PostProcessorRegisterPostProcessor());

		this
				.registerResourcePostProcessor(new ConverterRegisterPostProcessor());
		this.registerResourcePostProcessor(new ScopedBeanPostProcessor());

		this.initializer = new BeanInitializingPostProcessor();
	}

	/**
	 * Initializes and wires a bean to it's dependencies and dependents.
	 */
	@Override
	protected Object initializeBean(BeanDefinition beanDef) {
		if (debug)
			log.debug("Initializing bean: " + beanDef);
		// TODO Refactor this guy here
		try {
			beanDef.initialize();
			// Instantiate the bean
			Object beanInstance = this.instantiateBean(beanDef);

			// Inject it dependencies
			injectResourceDependencies(beanDef, beanInstance);

			// Post process it
			beanInstance = this.postProcessResource(beanDef, beanInstance);

			// The initializer post processor must run affter all post
			// processors
			if (this.initializer.mayProcess(beanDef)) {
				this.initializer.processBean(beanDef, beanInstance);
			}

			beanDef.initializationDone();

			// We're done here
			return beanInstance;

		} catch (Throwable t) {
			throw new BeanInitializationException(
					"Could not initialize the bean: \"" + beanDef.getName()
							+ '"', t);
		}
	}

	/**
	 * 
	 * Causes to run the bean post processors on the bean.
	 * 
	 * @param beanDef
	 *            The definition of the bean which is being processed.
	 * @param beanInstance
	 *            The curent bean instance
	 * @return A processed version of the bean instance
	 */
	private Object postProcessResource(BeanDefinition beanDef,
			Object beanInstance) {
		if (debug)
			log.debug("Post processing bean: " + beanDef);

		List<BeanPostProcessor> postProcessors = getPostProcessors();
		BeanPostProcessor processor;
		int size = postProcessors.size();

		for (int i = 0; i < size; i++) {

			processor = postProcessors.get(i);

			if (processor.mayProcess(beanDef)) {
				beanInstance = processor.processBean(beanDef, beanInstance);
			}
		}

		return beanInstance;
	}

	/**
	 * Gets a list with the {@link BeanPostProcessor} registered on this factory
	 * 
	 * @return The list with the registered processors
	 */
	private List<BeanPostProcessor> getPostProcessors() {
		return this.beanPostProcessors;
	}

	/**
	 * Resolves a collection of dependencies
	 * 
	 * @param dependencies
	 * @return
	 */
	private Object[] resolveDependencies(Dependency[] dependencies) {
		Object[] dependencies2 = new Object[dependencies.length];

		for (int i = 0; i < dependencies.length; i++) {
			dependencies2[i] = this.resolveDependency(dependencies[i]);
		}

		return dependencies2;
	}

	/**
	 * Resolve a dependency and returns it's value.
	 * 
	 * @param dependency
	 *            The dependency definition
	 * @return the resolved dependency
	 */
	private Object resolveDependency(Dependency dependency) {

		if (dependency instanceof ValueDependecy) {
			// Straight value dependecy
			ValueDependecy vd = (ValueDependecy) dependency;
			Class<?> type = vd.getType();

			String stringValue = vd.getStringValue();

			Converter converter = this.getConverterFor(type);

			return converter == null ? stringValue : converter.convert(
					stringValue, type);
		} else if (dependency instanceof ResourceReferenceDependency) {
			// Resource dependecy
			ResourceReferenceDependency rd = (ResourceReferenceDependency) dependency;

			String beanName = rd.getReferenceName();

			Object bean;

			// If the resource reference is not named, looks for a single bean
			// of the desired type
			if (!StringUtils.isEmpty(beanName)) {
				bean = this.getResource(beanName);
			} else {
				bean = this.getResource(rd.getType());
			}

			if (bean == null && dependency.isRequired())
				throw new BeanInitializationException(
						"Could not find required dependency: " + dependency);

			return bean;

		}
		return null;
	}

	/**
	 * Gets the registered {@link Converter} responsible for converting a string
	 * into the required type.
	 * 
	 * @param requiredType
	 *            The required type
	 * @return the binder
	 */
	private Converter getConverterFor(Class<?> requiredType) {
		Converter converters = convertersCache.get(requiredType);

		if (converters == null) {
			for (Converter b : this.converters) {
				if (b.accepts(requiredType)) {
					this.convertersCache.put(requiredType, b);
					return b;
				}
			}
		}

		return converters;
	}

	/**
	 * Register a new converter to the {@link BeanFactory}
	 * 
	 * @param converter
	 */
	public void addConverter(Converter converter) {
		Assert.notNull(converter);
		if (!this.converters.contains(converter))
			this.converters.add(converter);
	}

	/**
	 * Register a new bean processor to this {@link BeanFactory}
	 * 
	 * @param beanPostProcessor
	 */
	public void registerResourcePostProcessor(
			BeanPostProcessor beanPostProcessor) {
		Assert.notNull(beanPostProcessor);

		if (!this.beanPostProcessors.contains(beanPostProcessor)) {
			if (debug)
				log
						.debug("Registering BeanPostProcessor: "
								+ beanPostProcessor);
			this.beanPostProcessors.add(beanPostProcessor);
			beanPostProcessor.setBeanFactory(this);
		}

	}

	/**
	 * Instantiates the bean according to it's definitions and constructor
	 * arguments.
	 * 
	 * @param definition
	 *            The bean definition
	 * @return The instance of the bean
	 */
	private Object instantiateBean(BeanDefinition definition) {
		if (debug)
			log.debug("Instantiating bean: " + definition);
		// The instantiating class
		Class<?> instantiatingClass = definition.getInstantiatingClass();
		// The constructor arguments
		Dependency[] dependencies = definition
				.getDependencies(DependencyTarget.Constructor);

		ClassProxy<?> reflect = Reflect.on(instantiatingClass);

		// If there's no constructor argument, just instantiate it
		if (dependencies == null || dependencies.length == 0) {
			return reflect.newInstance();
		} else {
			// Resolve constructor arguments
			Object[] resolvedDependencies = this
					.resolveDependencies(dependencies);

			// Gets the classes of the arguments
			Class<?>[] arguments = getDependenciesClasses(dependencies);

			// Gets the constructor
			ConstructorProxy<?> constructor = reflect.constructor(arguments);

			// Invoke it
			return constructor.invoke(resolvedDependencies);
		}

	}

	/**
	 * Gets the types of an array of dependencies.
	 * <p>
	 * The types will be extracted using <code>getType()</code> method.
	 * 
	 * @param dependencies
	 *            A non-empty array of dependencies.
	 * @return The classes which represents the types of the dependencies
	 */
	private Class<?>[] getDependenciesClasses(Dependency[] dependencies) {
		Assert.notEmpty(dependencies);

		Class<?>[] classes = new Class<?>[dependencies.length];

		for (int i = 0; i < dependencies.length; i++) {
			classes[i] = dependencies[i].getType();
		}

		return classes;
	}

	/**
	 * Injects the dependencies into the bean instance according to it's
	 * definition
	 * 
	 * @param definition
	 *            The bean definition
	 * @param beanInstance
	 *            The bean instance
	 */
	private void injectResourceDependencies(BeanDefinition definition,
			Object beanInstance) {

		injectAttributes(definition, beanInstance);

		injectProperties(definition, beanInstance);
	}

	/**
	 * Inject the attribute dependencies to the bean
	 * 
	 * @param definition
	 *            The bean definition
	 * @param beanInstance
	 *            The bean instance
	 */
	private void injectAttributes(BeanDefinition definition, Object beanInstance) {
		Dependency[] dependencies = definition
				.getDependencies(DependencyTarget.Attribute);
		Object[] resolved = this.resolveDependencies(dependencies);

		ObjectProxy<Object> bean = Reflect.on(beanInstance);

		for (int i = 0; i < resolved.length; i++) {
			String property = dependencies[i].getPropertyName();

			bean.field(property).set(resolved[i]);
		}
	}

	/**
	 * Inject the dependencies using setter methods
	 * 
	 * @param definition
	 *            The bean definition
	 * @param beanInstance
	 *            The bean instance
	 */
	private void injectProperties(BeanDefinition definition, Object beanInstance) {
		Dependency[] dependencies = definition
				.getDependencies(DependencyTarget.Property);
		Object[] resolved = this.resolveDependencies(dependencies);

		ObjectProxy<Object> bean = Reflect.on(beanInstance);

		for (int i = 0; i < resolved.length; i++) {
			String setter = "set"
					+ StringUtils.capitalize(dependencies[i].getPropertyName());

			bean.method(setter, dependencies[i].getType()).invoke(resolved[i]);
		}
	}
}
